Option Explicit

'===================================================
' 文件操作
' 创建文件
'===================================================
' 将字符串写出到文件，文件编码格式： UTF-8(No Bom)
' @param path 文件路径
' @param text 写出的数据
Public Sub writeUtf8NobomFile(ByRef path As String, ByRef text As String)
On Error GoTo catch

    Dim utf8Stream As Object: Set utf8Stream = UtilsCreateSysObj.createAdodbStream
    Dim utf8NoBomStream As Object: Set utf8NoBomStream = UtilsCreateSysObj.createAdodbStream

    With utf8Stream
        .Charset = "UTF-8"
        .Open
        .writeText text
        .Position = 0
        .saveToFIle path, 2
        .Type = 2
        .Position = 3
    End With

    With utf8NoBomStream
        .Type = 1
        .Open
        utf8Stream.copyto utf8NoBomStream
        .saveToFIle path, 2
    End With

    UtilsCreateSysObj.closeAdodbStream utf8Stream
    UtilsCreateSysObj.closeAdodbStream utf8NoBomStream

    Exit Sub

catch:
    UtilsCreateSysObj.closeAdodbStream utf8Stream
    UtilsCreateSysObj.closeAdodbStream utf8NoBomStream

    Err.Raise Err.Number, Description:=Err.Description
End Sub

' 创建一个空文件，文件编码格式： UTF-8(No Bom)
' @param path 文件路径
Public Sub writeUtf8NobomEmptyFile(ByRef path As String)
    writeUtf8NobomFile path, ""
End Sub

' 写出文件，使用系统默认的编码格式
' @param path 文件路径
' @param text 写出的数据
Public Sub writeTxtFile(ByRef path As String, ByRef text As String)
    Dim handler As Object
    Set handler = UtilsCreateSysObj.getGlobalFSO.CreateTextFile(path, True)
    handler.write text
    handler.Close

    'Open path For Output Access Write As #1
    '    Print #1, text
    'Close #1
End Sub

' 创建一个空文件，使用系统默认的编码格式
' @param path 文件路径
Public Sub writeTxtEmptyFile(ByRef path As String)
    writeTxtFile path, ""
End Sub

' 将字符串写出到文件，文件编码格式： UTF-8(No Bom)。如果文件路径不存在，将会自动创建整个路径
' @param path 文件路径
' @param text 写出的数据
Public Sub writeUtf8NobomFileAuto(ByRef path As String, ByRef text As String)
    ' (如果文件目录不存在，) 递归创建文件目录
    Dim fileDir as String
    fileDir = UtilsIO.dirname(path)
    UtilsIO.mkdirs fileDir

    writeUtf8NobomFile path, text
End Sub

' 创建一个空文件，文件编码格式： UTF-8(No Bom)。如果文件路径不存在，将会自动创建整个路径
' @param path 文件路径
Public Sub writeUtf8NobomEmptyFileAuto(ByRef path As String)
    writeUtf8NobomFileAuto path, ""
End Sub

' 写出文件，使用系统默认的编码格式。如果文件路径不存在，将会自动创建整个路径
' @param path 文件路径
' @param text 写出的数据
Public Sub writeTxtFileAuto(ByRef path As String, ByRef text As String)
    ' (如果文件目录不存在，) 递归创建文件目录
    Dim fileDir as String
    fileDir = UtilsIO.dirname(path)
    UtilsIO.mkdirs fileDir

    writeTxtFile path, text
End Sub

' 创建一个空文件，使用系统默认的编码格式。如果文件路径不存在，将会自动创建整个路径
' @param path 文件路径
Public Sub writeTxtEmptyFileAuto(ByRef path As String)
    writeTxtFileAuto path, ""
End Sub

'===================================================
' 文件操作
' 读文件
'===================================================
' 读取文件
' 无法处理 UTF-8 字符编码文件中的中文
' @param path 文件路径
' @return 文件的数据
Public Function readTxtFile(ByRef path As String) As String
    readTxtFile = ""
    Open path For Input As #1
        ' 废弃: 用下面一行全部读取的方式代替
        ' While Not EOF(1)
        '     Line Input #1, tmp
        '     readTxtFile = readTxtFile & tmp & vbLf
        ' Wend
        readTxtFile = Input$(LOF(1), #1)
    Close #1
End Function

' 以二进制读取文件，并将读取内容转换为16进制数字字符串的数组
' 这里不使用数组作为返回值。如果返回值是数组，在方法内外赋值时会产生数据复制
'
' @param path 文件路径
' @return 16进制数字字符串的数组
Public Function readFileToHexArray(ByRef path As String) As IPrimitiveArrayRef
    Dim i As Long
    Dim v As Byte
    Dim temp As String
    Dim size As Long

    ' 1. 打开文件
    Open path For Binary As #1

    ' 2. 读取文件长度，并初始化数组
    size = LOF(1)
    Set readFileToHexArray = UtilsArrayRef.createRealPrimitiveArrayRef(size)

    ' 3. 读取文件内数据
    For i = 0 To size - 1
        Get #1, , v

        temp = CStr(hex(v))
        If Len(temp) = 1 Then
            temp = "0" & temp
        End If
        readFileToHexArray.Add temp
    Next i

    ' 4. 关闭文件
    Close #1
End Function

' 按照指定的编码读取文件，并返回文件内容的字符串
' @param path 文件路径
' @param encoding 文件编码
' @return String 文件内容的字符串
Public Function readFileWithEncoding(ByRef path As String, ByRef encoding As String) As String
On Error Goto catch:
    Dim strFileContent As String
    Dim strFilePath As String

    Dim stream As Object
    Set stream = UtilsCreateSysObj.createAdodbStream

    ' 设置流类型
    With stream
        .Type = 2 ' adTypeText

        ' 设置文件编码
        .Charset = encoding

        ' 打开流
        .Open

        ' 加载文件
        .LoadFromFile path

        ' 读取数据
        readFileWithEncoding = .ReadText

        ' 关闭流
        .Close
    End With
    Set stream = Nothing
    Exit Function

catch:
    If Not stream Is Nothing Then
        stream.Close
        Set stream = Nothing
    End If

    Err.Raise Err.Number, Description:=Err.Description
End Function

' 按照UTF-8编码读取文件，并返回文件内容的字符串
'
' @param path 文件路径
' @return String 文件内容的字符串
Public Function readFileWithUtf8(ByRef path As String) As String
    readFileWithUtf8 = readFileWithEncoding(path, "utf-8")
End Function

' 以二进制方式打开文件，并读取文件中的内容为byte数组引用
'
' @param path 文件路径
' @return IPrimitiveArrayRef byte 数组引用
Public Function readFileByteArray(ByRef path As String) As IPrimitiveArrayRef
    Dim size As Long
    Dim v As Byte

    Open path For Binary As #1
    size = LOF(1)

    Set readFileByteArray = UtilsArrayRef.createRealPrimitiveArrayRef(size)

    Dim i As Integer
    For i = 0 To size - 1
        Get #1, , v
        readFileByteArray.Add v
    Next i

    Close #1
End Function

'===================================================
' 将文件读取成Collection
'===================================================
' 按照UTF-8编码读取文件，并按照换行符将文件内容分割为 Collection
' 读取时，UTF-8 文件开头的 `BOM` 将会自动丢弃
' 注意事项:
'    使用该方法读取文件时，文件中的中文字符不会乱码
'
' @param path 文件路径
' @return 文件内容的字符串列表
Public Function readFileToCollectionWithUtf8(ByRef path As String) As Collection
    Set readFileToCollectionWithUtf8 = New Collection

    ' 1. 读取文件
    Dim str As String
    str = readFileWithUtf8(path)

    ' 2. 分割文件内容
    '    这里只考虑两种换行符: vbLF, vbCrLf
    '    使用 Lf 来分割，然后删除默认的 Cr
    Dim strArray() As String
    strArray = Split(str, vbLf)

    Dim i As Integer
    Dim tmp As String
    For i = 0 To UBound(strArray)
        tmp = strArray(i)
        If Right(tmp, 1) = VBCr Then
            readFileToCollectionWithUtf8.Add Left(tmp, Len(tmp) - 1)
        Else
            readFileToCollectionWithUtf8.Add tmp
        End If
    Next
End Function

'===================================================
' 文件的拆分与合并
'===================================================
' 按照指定文件大小拆分文件
' @param srcPath, 原始文件路径
' @param descDir, 保存拆分后文件的目录路径
' @param descName, 拆分后的文件名
'                  拆分后，会在 `descName` 后面附加 `_ID`。`ID` 表示文件拆分时的顺序，从 1 开始
' @optional-param fileSize, 拆分后，每个文件的最大长度
'                 单位 = 字节
' @return 原始文件拆分后的文件数量
Public Function splitFileBySize(ByRef srcPath as String, ByRef descDir as String, ByRef descName as String, Optional ByRef fileSize As Long = 1024) As Long
    ' 0. 参数检查
    ' srcPath 不能是空字符串，文件必须存在
    If srcPath = "" Then
        Err.Raise 10007, Description:="Method[splitFileBySize]'s String parameter[srcPath] must not be empty string."
    End If
    If UtilsIO.pathExist(srcPath) = False Then
        Err.Raise 10014, Description:="Method[splitFileBySize]'s parameter[srcPath]: Path[" & srcPath & "] is not exist"
    End If

    ' descDir 不能是空字符串，路径必须存在
    If descDir = "" Then
        Err.Raise 10007, Description:="Method[splitFileBySize]'s String parameter[descDir] must not be empty string."
    End If

    If UtilsIO.pathExist(descDir) = False Then
        Err.Raise 10015, Description:="Method[splitFileBySize]'s parameter[descDir]: Path[" & srcPath & "] is not exist"
    End If

    ' descName 不能是空字符串
    If descName = "" Then
        Err.Raise 10007, Description:="Method[splitFileBySize]'s String parameter[descName] must not be empty string."
    End If

    ' fileSize 必须大于 0
    If fileSize <= 0 Then
        Err.Raise 10016, Description:="Method[splitFileBySize]'s parameter[fileSize]: Integer Or Long parameter's value must not be greater than 0. Now parameter is " & fileSize
    End If

    ' 1. 解析文件名中的真实名称 + 文件扩展名
    '    然后在真实名称前面附加拆分后的文件目录路径

    ' 在真实名称前面将会附加文件目录路径
    ' 在真实名称的末尾会附加一个`_`，如: `xxx_`
    Dim descFilePath As String
    ' 在开头包含 `.` ，如: `.txt`
    Dim descFileExtension As String

    ' 初始化路径
    If Right(descDir, 1) = "\" Then
        descFilePath = descDir
    Else
        descFilePath = descDir & "\"
    End If

    ' 为了防止一个文件名中存在多个 `.`，从右向左扫描
    Dim i As Long
    For i = Len(descName) To 1 Step -1
        If Mid(descName, i, 1) = "." Then
            ' 发现 `.` 后，将文件名称部分保存到路径中
            ' 并保存文件扩展名
            descFilePath = descFilePath & Right(descName, i - 1) & "_"
            descFileExtension = Right(descName, Len(descName) - i + 1)
            Exit For
        End If
    Next
    ' 如果扩展名仍然是空的，则说明文件名中没有扩展名，则需要将 descName 添加到路径中
    If descFileExtension = "" Then
        descFilePath = descFilePath & descName & "_"
    End If

    ' 2. 打开输入文件
    Open srcPath For Binary Access Read As #1

    ' 3. 读取原始文件长度，并初始化数组
    Dim maxIndex As Long
    maxIndex = LOF(1) - 1

    ' 4. 读取文件内数据，并输出到 descPath 文件
    ' 未读取的长度
    Dim notReadSize As Long
    ' 输入数据的 buf
    Dim inputBuf() As Byte
    ReDim inputBuf(0 To 1023)

    ' 子文件的 ID (从 1 开始)
    Dim descFileId As Long
    descFileId = 1
    If fileSize = 1024 Then
        ' 4.1 如果 fileSize == 1024， 则直接将 inputBuf 输出到文件
        For i = 0 To maxIndex Step 1024
            ' 4.1.1 计算这一次的读取长度
            notReadSize = maxIndex - i + 1

            ' 4.1.2 读取数据
            If notReadSize >= 1024 Then
                Get #1, , inputBuf
            Else
                ' 到这里说明是最后一次
                ReDim inputBuf(0 To (notReadSize - 1))
                Get #1, , inputBuf
            End If

            ' 4.1.3 输出 buf 中的数据到子文件中
            Open (descFilePath & descFileId & descFileExtension) For Binary Access Write As #2
            Put #2, , inputBuf
            Close #2

            descFileId = descFileId + 1
        Next i
    Else
        ' 4.2 如果 fileSize != 1024
        ' 这一次读取的数据长度
        Dim inputByteSize As Integer
        ' inputBuf 中未使用的长度
        Dim unusedInputByteSize As Integer

        ' 输出的 buf
        Dim outputBuf() As Byte
        ReDim outputBuf(0 To (fileSize - 1)) As Byte
        ' outputBuf 中未填充的字节数量 (每次需要重置为, fileSize)
        Dim outputBufNotFillByteSize As Long
        outputBufNotFillByteSize = fileSize

        Dim j As Long
        Dim inputStartIndex As Long
        Dim outputStartIndex As Long
        Dim needWriteSize As Long
        For i = 0 To maxIndex Step 1024
            ' 4.2.1 计算这一次的读取长度
            notReadSize = maxIndex - i + 1
            If notReadSize >= 1024 Then
                inputByteSize = 1024
            Else
                ' 到这里说明是最后一次
                inputByteSize = notReadSize
            End If

            ' 4.2.2 读取数据
            Get #1, , inputBuf

            ' 4.2.3 遍历 inputBuf, 并将数据转移到 outputBUf输出 buf 中的数据到子文件中
            unusedInputByteSize = inputByteSize
            while unusedInputByteSize > 0
                ' 4.2.3.1 计算遍历的长度
                If outputBufNotFillByteSize <= unusedInputByteSize Then
                    ' 如果 outputBuf 中未写入的字节数量 <= inputBuf 中未使用的字节数量
                    ' 按照 outputBufNotFillByteSize 来遍历 inputBuf
                    needWriteSize = outputBufNotFillByteSize
                Else
                    ' 否则，按照 unusedInputByteSize 的长度来处理
                    needWriteSize = unusedInputByteSize
                End If

                ' 4.2.3.2 转移数据
                inputStartIndex = inputByteSize - unusedInputByteSize
                outputStartIndex = fileSize - outputBufNotFillByteSize
                For j = 0 To (needWriteSize - 1)
                    outputBuf(outputStartIndex + j) = inputBuf(inputStartIndex + j)
                Next

                ' 4.2.3.3 重新计算数量
                outputBufNotFillByteSize = outputBufNotFillByteSize - needWriteSize
                unusedInputByteSize = unusedInputByteSize - needWriteSize

                ' 4.2.3.4 检查，如果 outputBufNotFillByteSize == 0 ，则将 outputBuf 输出到文件
                '         然后重置 outputBufNotFillByteSize = fileSize
                If outputBufNotFillByteSize = 0 Then
                    Open (descFilePath & descFileId & descFileExtension) For Binary Access Write As #2
                    Put #2, , outputBuf
                    Close #2

                    descFileId = descFileId + 1

                    outputBufNotFillByteSize = fileSize
                End If
            Wend
        Next i
    End If

    ' 5. 检查: 如果 outputBufNotFillByteSize < fileSize，则输出到文件中
    If outputBufNotFillByteSize < fileSize Then
        ' 重置数组大小
        ReDim Preserve outputBuf(0 To (fileSize - outputBufNotFillByteSize - 1))

        ' 输出到文件
        Open (descFilePath & descFileId & descFileExtension) For Binary Access Write As #2
        Put #2, , outputBuf
        Close #2
    End If

    ' 6. 关闭原始文件
    Close #1

    ' 7. 返回文件数量
    splitFileBySize = descFileId - 1
End Function


' 按照顺序合参数 `fileNames` 指定的所有文件
'
' @param descDir, 保存合并后的文件的目录路径
' @param descName, 合并后的文件名
' @param fileNames, 所有需要合并的文件路径
Public Sub mergeFiles(ByRef descDir as String, ByRef descName as String, ParamArray fileNames() As Variant)
    ' 0. 参数检查
    ' descDir 不能是空字符串，路径必须存在
    If descDir = "" Then
        Err.Raise 10007, Description:="Method[mergeFiles]'s String parameter[descDir] must not be empty string."
    End If

    If UtilsIO.pathExist(descDir) = False Then
        Err.Raise 10015, Description:="Method[mergeFiles]'s parameter[descDir]: Path[" & descDir & "] is not exist"
    End If

    ' descName 不能是空字符串
    If descName = "" Then
        Err.Raise 10007, Description:="Method[mergeFiles]'s String parameter[descName] must not be empty string."
    End If

    ' fileNames 数量必须大于 0
    If UBound(fileNames) = -1 Then
        Err.Raise 10006, Description:="Method[mergeFiles]: ParamArray parameter[fileNames]'s size must not be 0"
    End If

    ' 1. 创建输出文件路径
    Dim descFilePath As String
    If Right(descDir, 1) = "\" Then
        descFilePath = descDir & descName
    Else
        descFilePath = descDir & "\" & descName
    End If

    ' 2. 打开输出文件
    Open descFilePath For Binary Access Write As #1

    ' 3. 遍历所有需要合并的文件
    Dim i As Integer
    Dim j As Long

    ' 文件的未读取的长度
    Dim notReadSize As Long
    ' 文件的最大索引
    Dim maxIndex As Long

    ' 输入数据 buf
    Dim inputBuf(0 To 1023) As Byte
    ' 动态 buf, 用于在长度不足 1024 时读取数据
    Dim dynamicBuf() As Byte
    For i = 0 To UBound(fileNames)
        ' 3.1 打开一个需要合并的文件
        Open fileNames(i) For Binary Access Read As #2

        ' 3.2 获取文件的最大索引值
        maxIndex = LOF(2) - 1

        ' 3.3 读取并输出数据
        For j = 0 To maxIndex Step 1024
            ' 3.3.1 计算这一次的读取长度
            notReadSize = maxIndex - j + 1

            ' 3.3.2 读取数据并写入输出文件
            If notReadSize >= 1024 Then
                Get #2, , inputBuf
                Put #1, , inputBuf
            Else
                ' 到这里说明是这个文件的最后一次读取
                ReDim dynamicBuf(0 To (notReadSize - 1))
                Get #2, , dynamicBuf
                Put #1, , dynamicBuf
            End If
        Next

        Close #2
    Next

    ' 关闭输出文件
    Close #1
End Sub

'===================================================
' 文件操作
' 拷贝文件数据
'===================================================
' 文件拷贝
' @param src 原始文件路径
' @param desc 目标文件路径
Public Sub copyFile(ByRef src As String, ByRef desc As String)
    FileCopy src, desc
End Sub

' 拷贝字符串到剪贴板
' @param text 需要拷贝的字符串
Public Sub copyStr2Clipboard(ByRef text As String)
    Dim dataObj As DataObject
    Set dataObj = New DataObject

    dataObj.Clear
    dataObj.SetText text
    dataObj.PutInClipboard
End Sub


' 以二进制读取 `srcPath` 文件，并将读取内容转换为16进制数字字符串，然后将其输出到 `desc` 文件内
' 每个 16 进制数字字符串之间通过空格分隔
'
' @parm srcPath 输入文件路径
' @parm descPath 输出文件路径
Public Sub copyFileToHexStrFile(ByRef srcPath As String, ByRef descPath As String)
    Dim i As Long
    Dim v As Byte
    Dim temp As String
    Dim maxIndex As Long

    ' 1. 打开两个文件
    Open srcPath For Binary Access Read As #1
    Open descPath For Output As #2

    ' 2. 读取文件长度，并初始化数组
    maxIndex = LOF(1) - 1

    ' 3. 先读取一个字节的数据
    Get #1, , v

    ' 从输入文件中读取一个字节
    temp = CStr(hex(v))
    If Len(temp) = 1 Then
        temp = "0" & temp
    End If

    Dim outputBuf As String
    outputBuf = temp

    ' 4. 读取文件内数据，并输出到 descPath 文件
    ' 未读取的长度
    Dim notReadSize As Long
    ' 这一次读取的数据长度
    Dim inputByteSize As Integer

    ' 输入数据的 buf
    Dim inputBuf(0 To 1023) As Byte

    Dim j As Integer
    Dim c As String
    For i = 1 To maxIndex Step 1024
        ' 4.1 计算这一次的读取长度
        notReadSize = maxIndex - i + 1
        If notReadSize >= 1024 Then
            inputByteSize = 1024
        Else
            ' 到这里说明是最后一次
            inputByteSize = notReadSize
        End If

        ' 4.2 读取数据
        Get #1, , inputBuf

        ' 4.3 处理每一个字符
        For j = 0 To inputByteSize - 1
            temp = CStr(Hex(inputBuf(j)))

            If Len(temp) = 1 Then
                outputBuf = outputBuf & " 0" & temp
            Else
                outputBuf = outputBuf & " " & temp
            End If
        Next

        ' 3.2 将转换后的 16 进制数字符串
        Print #2, outputBuf;
        outputBuf = ""
    Next i

    ' 5. 如果存在未输出的内容则输出
    If outputBuf <> "" Then
        Print #2, outputBuf;
    End If

    ' 4. 关闭文件
    Close #1
    Close #2
End Sub

' 读取 `hexStrFilePath` 指定的16进制数字字符串文件，并将其中的内容还原成二进制数据，然后输出为一个真实文件
' 每个 16 进制数字字符串之间应该通过空格分隔
'
' @param hexStrFilePath 16进制数字字符串文件
' @param realFilePath 真实文件路径
Public Sub copyHexStrToRealFile(ByRef hexStrFilePath As String, ByRef realFilePath As String)
    Dim i As Long
    Dim v As String
    Dim temp As String
    Dim hexStr As String
    Dim maxIndex As Long
    Dim b As Byte

    ' 0. 先删除输出文件，以保证二进制输出文件时，如果新文件的长度 < 旧文件的长度
    '    旧文件末尾多余字符不会残留在文件中
    UtilsIO.tryDeleteFile realFilePath

    ' 1. 打开两个文件
    Open hexStrFilePath For Input As #1
    Open realFilePath For Binary Access Write As #2

    ' 2. 读取文件长度，并初始化数组
    maxIndex = LOF(1) - 1

    ' 3. 读取文件内数据，并输出到 descPath 文件
    '    每次最多读取 1023 个字符
    '    1023 是 3 的倍数，比较好处理最后一个后面没有空格的16进制数字
    '
    '    输出的 buf 长度为 1024，以降低输出的次数

    ' 创建输出 buf
    Dim buf() As Byte
    ReDim buf(0 To 1023)
    Dim bufSize As Integer
    bufSize = 0

    ' 未读取的长度
    Dim notReadSize As Long
    ' 读取的字符长度
    Dim inputCharSize As Integer

    Dim j As Integer
    For i = 0 To maxIndex Step 1023
        ' 3.1 每次最多读取 1023 个字符
        notReadSize = maxIndex - i + 1
        If notReadSize >= 1023 Then
            hexStr = Input(1023 , #1)
            inputCharSize = 1023
        Else
            hexStr = Input(notReadSize , #1)
            inputCharSize = notReadSize
        End If

        ' 3.2 以 3 为步长，每次截取两个字符
        For j = 1 To inputCharSize Step 3
            ' 3.4 将数据保存到 buf
            buf(bufSize) = Application.Hex2Dec(Mid(hexStr, j, 2))
            bufSize = bufSize + 1

            ' 3.5 如果已经到达了 buf 的上限则输出到文件
            If bufSize = 1024 Then
                Put #2, , buf
                bufSize = 0
            End If
        Next
    Next i

    If bufSize > 0 Then
        ReDim Preserve buf(0 To (bufSize - 1))
        Put #2, , buf
    End If

    ' 4. 关闭文件
    Close #1
    Close #2
End Sub

'===================================================
' 文件操作
' 删除文件
'===================================================
' 如果文件存在，则删除文件
' @param path 文件路径
Public Sub tryDeleteFile(ByRef path As String)
    Dim fso As Object: Set fso = UtilsCreateSysObj.getGlobalFSO

    If fso.FileExists(path) Then
        fso.deleteFile path, True
    End If
End Sub

'===================================================
' 文件操作
' 获取文件信息
'===================================================
' 获取文件的长度
' @param path 文件路径
' @return Double
Public Function getFileByteCount(ByRef path As String) As Double
    getFileByteCount = UtilsCreateSysObj.getGlobalFSO.GetFile(path).Size
End Function

' 获取文件后缀
' @param path 文件路径
' @return 文件后缀
Public Function extension(ByRef path As String) As String
    If path = "" Then
        extension = ""
        Exit Function
    End If

    ' get index of last /
    Dim lastPathSep As Integer
    lastPathSep = InStrRev(path, "\")

    Dim extensionStart As Integer
    If lastPathSep = 0 Then
        extensionStart = InStrRev(path, ".")
    Else
        extensionStart = InStr(lastPathSep, path, ".")
    End If

    extension = Mid(path, extensionStart + 1)
End Function

' 删除文件名的扩展名
' @param fileName 文件名
' @return 不包含后缀的文件名
Public Function cutExt(ByRef fileName As String) As String
    Dim pointIndex As Integer
    pointIndex = InStr(fileName, ".")
    If pointIndex = 0 Or pointIndex = Len(fileName) Then
        ' 如果不存在 . 或者 . 是文件名中的最后一个字符，如 abc. 则不做修改，直接返回
        cutExt = fileName
    Else
        cutExt = Mid(fileName, 1 , pointIndex - 1)
    End If
End Function

' 获取一个文件的无扩展名的文件名
' @param path 目录路径
' @return String 文件的无扩展名的文件名
Public Function getNoExtFileName(ByRef path As String) As String
    ' ==================================================
    ' 1. 获取最后一个路径分隔符 `\` 或 `/` 的位置，来确定起始索引
    ' ==================================================
    Dim startIndex As Integer

    ' 1. 先搜索 `\`
    startIndex = InStrRev(path, "\")

    ' 2. 如果没有找到 `\`，则再搜索 /
    If startIndex = 0 Then
        startIndex = InStrRev(path, "/")
    End If

    ' 3. 将起始索引定位到路径分隔符的后面
    startIndex = startIndex + 1

    ' ==================================================
    ' 2. 搜索第一个 . 的索引
    ' ==================================================
    Dim firstPointIndex As Integer
    firstPointIndex = InStr(startIndex, path, ".")

    ' 如果文件名中不存在 `.`，则直接截取字符串
    If firstPointIndex = 0 Then
        getNoExtFileName = Right(path, Len(path) - startIndex + 1)
        Exit Function
    End If

    ' ==================================================
    ' 3. 截取文件全名
    ' ==================================================
    getNoExtFileName = Mid(path, startIndex, firstPointIndex - startIndex)
End Function

' 获取一个文件名的第 N 级扩展名
' 文件的扩展名的级别从右开始逐级递增: `文件名.第N级扩展名.第N-1级扩展名....第1级扩展名`
'   如: `myFile.aaa.bbb.ccc.ddd`，第一级扩展名是 `ddd`，第二级扩展名是 `ccc`，第三级扩展名是 `bbb`，第四级扩展名是 `aaa`
' @param path 目录路径
' @param level 扩展名级别
' @return String 第 N 级扩展名
Public Function getFileNthLevelExtension(ByRef path As String, ByRef level As Integer) As String
    ' ==================================================
    ' 1. 检查参数
    ' ==================================================
    ' 1. 如果扩展名级别不符合规则，则返回空字符串
    If level < 1 Then
        Exit Function
    End If

    ' ==================================================
    ' 2. 截取文件名
    ' ==================================================
    Dim curIndex As Integer

    ' 1. 先搜索 `\`
    curIndex = InStrRev(path, "\")

    ' 2. 如果没有找到 `\`，则再搜索 /
    If curIndex = 0 Then
        curIndex = InStrRev(path, "/")
    End If

    ' 3. 将起始索引定位到路径分隔符的后面
    Dim fileName As String
    If curIndex = 0 Then
        fileName = path
    Else
        fileName = Right(path, Len(path) - curIndex)
    End If

    ' 4. 如果文件名是空字符串，则结束，并返回空字符串
    If fileName = "" Then
        Exit Function
    End If

    ' ==================================================
    ' 3. 搜索最后一个 `.` 的索引
    ' ==================================================
    ' 1. 获取最后一个 `.` 的索引
    curIndex = InStrRev(fileName, ".")

    ' 2. 如果不存在 `.` ，则返回空字符串
    If curIndex = 0 Then
        Exit Function
    End If

    ' 3. 如果需要的是第 1 级扩展名，则截取字符串并返回
    If level = 1 Then
        getFileNthLevelExtension = Right(fileName, Len(fileName) - curIndex)
        Exit Function
    End If

    ' ==================================================
    ' 4. 搜索指定级别的 `.` ，同时保持前一个级别的 `.`
    ' ==================================================
    ' 1. 遍历搜索指定级别的 `.`
    Dim i As Integer
    Dim j As Integer
    Dim prevPointIndex As Integer

    ' 1.1 需要遍历处 level 个 `.` 才结束
    For i = 2 To level
        ' 1.2 暂存上一个 `.` 的索引
        prevPointIndex = curIndex

        ' 1.3 重制为 -1，以保证可以检查出这一轮是否找到了 `.`
        curIndex = -1

        ' 1.4 从后向前搜索 `.`，发现一个 `.` 后立刻退出这一级便利
        For j = prevPointIndex - 1 To 1 Step -1
            If Mid(fileName, j, 1) = "." Then
                curIndex = j
                Exit For
            End If
        Next

        ' 1.5 如果在循环检查时，就已经找不到 `.` 了，则停止，并返回空字符串
        If curIndex = -1 Then
            Exit Function
        End If
    Next

    ' 2. 截取字符串
    getFileNthLevelExtension = Mid(fileName, curIndex + 1, prevPointIndex - curIndex - 1)

End Function

' 获取一个文件名的第 1 级扩展名
' 文件的扩展名的级别从右开始逐级递增: `文件名.第N级扩展名.第N-1级扩展名....第1级扩展名`
'   如: `myFile.aaa.bbb.ccc.ddd`，第一级扩展名是 `ddd`，第二级扩展名是 `ccc`，第三级扩展名是 `bbb`，第四级扩展名是 `aaa`
' @param path 目录路径
' @return String 第 1 级扩展名
Public Function getFile1stLevelExtension(ByRef path As String) As String
    getFile1stLevelExtension = getFileNthLevelExtension(path, 1)
End Function

' 获取一个文件名的第 2 级扩展名
' 文件的扩展名的级别从右开始逐级递增: `文件名.第N级扩展名.第N-1级扩展名....第1级扩展名`
'   如: `myFile.aaa.bbb.ccc.ddd`，第一级扩展名是 `ddd`，第二级扩展名是 `ccc`，第三级扩展名是 `bbb`，第四级扩展名是 `aaa`
' @param path 目录路径
' @return String 第 2 级扩展名
Public Function getFile2ndLevelExtension(ByRef path As String) As String
    getFile2ndLevelExtension = getFileNthLevelExtension(path, 2)
End Function

' 获取一个文件名的第 3 级扩展名
' 文件的扩展名的级别从右开始逐级递增: `文件名.第N级扩展名.第N-1级扩展名....第1级扩展名`
'   如: `myFile.aaa.bbb.ccc.ddd`，第一级扩展名是 `ddd`，第二级扩展名是 `ccc`，第三级扩展名是 `bbb`，第四级扩展名是 `aaa`
' @param path 目录路径
' @return String 第 3 级扩展名
Public Function getFile3rdLevelExtension(ByRef path As String) As String
    getFile3rdLevelExtension = getFileNthLevelExtension(path, 3)
End Function

'===================================================
' 目录操作
' 目录检查
'===================================================
' 检查目录是否存在
' @param path 目录路径
' @return Boolean
Public Function dirExist(ByRef path As String) As Boolean
    dirExist = UtilsCreateSysObj.getGlobalFSO.folderExists(path)
End Function

' 检查指定目录是不是一个空目录
' 注意事项:
'   如果指定目录不存在，则默认返回 `True`
'
' @param path 目录路径
' @return Boolean
Public Function isEmptyDir(ByRef path As String) As Boolean
    ' 1. 如果指定目录不存在，则默认返回 `True`
    If dirExist(path) = False Then
        isEmptyDir = True
        Exit Function
    End If

    Dim checkPath As String
    checkPath = path
    If Right(checkPath, 1) <> "\" Then
        checkPath = checkPath & "\"
    End If

    isEmptyDir = True

    ' 检查目录下是否存在文件
    If Dir(checkPath & "*.*", vbNormal + vbHidden + vbSystem) <> "" Then
        isEmptyDir = False
        Exit Function
    End If

    ' 检查目录下是否存在子目录
    ' 需要忽略 . 和 .. 目录
    Dim dirName As String
    dirName = Dir(checkPath, vbDirectory + vbHidden)
    Do While dirName <> ""
        If dirName <> "." And dirName <> ".." Then
            If (GetAttr(checkPath & dirName) And vbDirectory) = vbDirectory Then
                isEmptyDir = False
                Exit Function
            End If
        End If
        dirName = Dir()
    Loop
End Function

' 目录移动
'
' @param src 原始目录
' @param desc 目标目录
Public Sub mvDir(ByRef src As String, ByRef desc As String)
    UtilsCreateSysObj.getGlobalFSO.MoveFolder src, desc
End Sub

'===================================================
' 目录操作
' 创建、删除目录
'===================================================
' 如果目录不存在，则创建目录
' @param path 目录路径
Public Sub tryCreateFolder(ByRef path As String)
    If Len(Dir(path, vbDirectory)) = 0 Then
        MkDir path
    End If
End Sub

' 递归创建目录
Public Sub mkdirs(ByRef path As String)
    If UtilsIO.pathExist(path) Then
        Exit Sub
    End If

    Dim splitor() As String
        splitor = Split(path, "\")

    Dim i As Integer
    Dim p As String
    Dim atePath As String

    ' 第一个路径是盘符不做处理
    atePath = splitor(0)

    ' 搜索第一个不存在的子路径
    Dim j As Integer
    For i = 1 To UBound(splitor)
        p = splitor(i)
        atePath = atePath & "\" & p
        If UtilsIO.pathExist(atePath) = False Then
            MkDir atePath
            Exit For
        End If
    Next i

    ' 从当前位置开始创建所有子路径
    For j = i + 1 To UBound(splitor)
        p = splitor(j)
        atePath = atePath & "\" & p
        MkDir atePath
    Next j
End Sub

' 删除目录
Public Sub rmdir(ByRef path As String)
    UtilsCreateSysObj.getGlobalFSO.DeleteFolder path, True
End Sub

'===================================================
' 批量扫描
' 获取文件名
'===================================================
' 获取指定目录下的文件名列表（只返回一级目录下的内容，不会进行遍历）
' @param path 目录路径
' @optional param template （可以包含通配符的）文件名模板
' @return Collection
Public Function filenameListFromFolder(ByRef path As String, Optional ByRef template As String) As Collection
    Set filenameListFromFolder = New Collection

    Dim f As Object
    If template = "" Then
        For Each f In UtilsCreateSysObj.getGlobalFSO.GetFolder(path).Files
            filenameListFromFolder.Add (f.Name)
        Next
    Else
        For Each f In UtilsCreateSysObj.getGlobalFSO.GetFolder(path).Files
            If f.Name Like template Then
                filenameListFromFolder.Add (f.Name)
            End If
        Next
    End If
End Function

' 使用正则表达式检查指定目录下的文件名，返回指定目录下符合规则的文件名列表
' @param path 目录路径
' @param pattern 正则表达式模板
' @return Collection
Public Function filenameListFromFolderByPattern(ByRef path As String, ByRef pattern As String) As Collection
On Error GoTo catch
    Dim regexp As Object

    Set filenameListFromFolderByPattern = New Collection

    Dim f As Object
    Set regexp = UtilsCreateSysObj.createRegexp()
    regexp.Global = True
    regexp.Pattern = pattern
    For Each f In UtilsCreateSysObj.getGlobalFSO.GetFolder(path).Files
        If regexp.Test(f.Name) Then
            filenameListFromFolderByPattern.Add (f.Name)
        End If
    Next

    Exit Function
catch:

    Err.Raise Err.Number, Description:=Err.Description
End Function

' 在指定目录下，模糊检索文件名。只返回第一个检索到的文件的文件名
' @param path 目录路径
' @param template （可以包含通配符的）文件名模板
' @return 文件名
Public Function getFirstMatchedFilenameFromFolder(ByRef path As String, ByRef template As String) As String
    getFirstMatchedFilenameFromFolder = ""
    Dim f As Object
    For Each f In UtilsCreateSysObj.getGlobalFSO.GetFolder(path).Files
        If f.Name Like template Then
            ' 忽略 excel 的临时文件
            If Left(f.Name, 2) <> "~$" Then
                getFirstMatchedFilenameFromFolder = f.Name
                Exit Function
            End If
        End If
    Next
End Function

' 使用正则表达式检查指定目录下的文件名，返回指定目录下第一个符合规则的文件名
' @param rootPath 目录路径
' @param pattern 正则表达式模板
' @return 文件名
Public Function getFirstMatchedFilenameFromFolderByPattern(ByRef path As String, ByRef pattern As String) As String
    Dim regexp As Object

    getFirstMatchedFilenameFromFolderByPattern = ""
    Dim f As Object

    Set regexp = UtilsCreateSysObj.createRegexp()
    regexp.Global = True
    regexp.Pattern = pattern
    For Each f In UtilsCreateSysObj.getGlobalFSO.GetFolder(path).Files
        If regexp.Test(f.Name) Then
            getFirstMatchedFilenameFromFolderByPattern = f.Name
            Exit Function
        End If
    Next
End Function

'===================================================
' 批量扫描
' 获取文件路径
'===================================================
' 遍历 `path` 目录及其所有子目录，并获取目录下的所有文件路径
' 如果指定了参数 `template`，会按照模板匹配符合的文件名
' @param path 目录路径
' @param template（可以包含通配符的）文件名模板
' @return Collection
Public Function walkFolderAndGetFilePathList(ByRef path As String, Optional ByRef template As String = "") As Collection
    Set walkFolderAndGetFilePathList = New Collection
    doWalkFolderAndGetFilePathList walkFolderAndGetFilePathList, path, template
End Function

' 执行: 遍历 `path` 目录及其所有子目录，并获取目录下的所有文件路径
Private Sub doWalkFolderAndGetFilePathList(ByRef c As Collection, ByRef path As String, ByRef template As String)
    Dim f As Object
    Dim folder As Object
    Set folder = UtilsCreateSysObj.getGlobalFSO.GetFolder(path)

    ' 处理文件
    If template = "" Then
        For Each f In folder.Files
            c.Add (f.Path)
        Next
    Else
        For Each f In folder.Files
            If f.Name Like template Then
                c.Add (f.Path)
            End If
        Next
    End If

    ' 处理子目录
    For Each f In folder.SubFolders
        doWalkFolderAndGetFilePathList c, f.Path, template
    Next
End Sub

' 遍历 `path` 目录及其所有子目录，使用正则表达式检查文件路径，收集并返回符合规则的文件路径
' @param path 目录路径
' @param pattern 正则表达式模板
' @return Collection
Public Function walkFolderAndGetFilePathListByPattern(ByRef path As String, ByRef pattern As String) As Collection
    If pattern = "" Then
        ' 如果参数 `pattern` 是空字符串，则将会调用 `walkFolderAndGetFilePathList` 来获取所有文件路径
        Set walkFolderAndGetFilePathListByPattern = walkFolderAndGetFilePathList(path)
        Exit Function
    End If

    ' 按照正则表达式来匹配文件路径
    Dim regexp As Object
    Set regexp = UtilsCreateSysObj.createRegexp()
    regexp.Global = True
    regexp.Pattern = pattern

    Set walkFolderAndGetFilePathListByPattern = New Collection
    doWalkFolderAndGetFilePathListByPattern walkFolderAndGetFilePathListByPattern, path, regexp
End Function

' 执行: 遍历 `path` 目录及其所有子目录，使用正则表达式检查文件路径，收集并返回符合规则的文件路径
Private Sub doWalkFolderAndGetFilePathListByPattern(ByRef c As Collection, ByRef path As String, ByRef regexp As Object)
    Dim f As Object
    Dim folder As Object
    Set folder = UtilsCreateSysObj.getGlobalFSO.GetFolder(path)

    ' 处理文件
    For Each f In folder.Files
        If regexp.Test(f.Path) Then
            c.Add (f.Path)
        End If
    Next

    ' 处理子目录
    For Each f In folder.SubFolders
        doWalkFolderAndGetFilePathListByPattern c, f.Path, regexp
    Next
End Sub

' 获取指定目录下的文件路径列表（只返回一级目录下的内容，不会进行遍历）
' @param path 目录路径
' @optional param template （可以包含通配符的）文件名模板
' @return Collection
Public Function filepathListFromFolder(ByRef path As String, Optional ByRef template As String) As Collection
    Set filepathListFromFolder = New Collection

    Dim f As Object
    If template = "" Then
        For Each f In UtilsCreateSysObj.getGlobalFSO.GetFolder(path).Files
            filepathListFromFolder.Add (f.Path)
        Next
    Else
        For Each f In UtilsCreateSysObj.getGlobalFSO.GetFolder(path).Files
            If f.Name Like template Then
                filepathListFromFolder.Add (f.Path)
            End If
        Next
    End If
End Function

' 使用正则表达式检查指定目录下的文件名，返回指定目录下符合规则的文件路径列表
' @param path 目录路径
' @param pattern 正则表达式模板
' @return Collection
Public Function filepathListFromFolderByPattern(ByRef path As String, ByRef pattern As String) As Collection
    Dim regexp As Object

    Set filepathListFromFolderByPattern = New Collection

    Dim f As Object
    Set regexp = UtilsCreateSysObj.createRegexp()
    regexp.Global = True
    regexp.Pattern = pattern
    For Each f In UtilsCreateSysObj.getGlobalFSO.GetFolder(path).Files
        If regexp.Test(f.Name) Then
            filepathListFromFolderByPattern.Add (f.Path)
        End If
    Next
End Function

' 在指定目录下，模糊检索文件名。只返回第一个检索到的文件的文件路径
' @param path 目录路径
' @param template （可以包含通配符的）文件名模板
' @return 文件名
Public Function getFirstMatchedFilepathFromFolder(ByRef path As String, ByRef template As String) As String
    getFirstMatchedFilepathFromFolder = ""
    Dim f As Object
    For Each f In UtilsCreateSysObj.getGlobalFSO.GetFolder(path).Files
        If f.Name Like template Then
            ' 忽略 excel 的临时文件
            If Left(f.Name, 2) <> "~$" Then
                getFirstMatchedFilepathFromFolder = f.Path
                Exit Function
            End If
        End If
    Next
End Function

' 使用正则表达式检查指定目录下的文件名，返回指定目录下第一个符合规则的文件路径
' @param rootPath 目录路径
' @param pattern 正则表达式模板
' @return 文件名
Public Function getFirstMatchedFilepathFromFolderByPattern(ByRef path As String, ByRef pattern As String) As String
    Dim regexp As Object

    getFirstMatchedFilepathFromFolderByPattern = ""
    Dim f As Object

    Set regexp = UtilsCreateSysObj.createRegexp()
    regexp.Global = True
    regexp.Pattern = pattern
    For Each f In UtilsCreateSysObj.getGlobalFSO.GetFolder(path).Files
        If regexp.Test(f.Name) Then
            getFirstMatchedFilepathFromFolderByPattern = f.Path
            Exit Function
        End If
    Next
End Function

'===================================================
' 路径操作
'===================================================
' 检查路径是否存在
' @param path 文件路径
' @return Boolean
Public Function pathExist(ByRef path As String) As Boolean
    pathExist = Dir(path, vbDirectory) <> ""
End Function

' 检查一个路径是不是文件
' 如果路径不存在，也会返回 `False`
'
' @param path 路径
' @return Boolean True=是文件，False=不是文件(不一定是目录，也许是路径不存在)
Public Function isFile(ByRef path As String) As Boolean
On Error Goto catch:
    isFile = GetAttr(path) = vbArchive
    Exit Function
catch:
    Err.Clear
    isFile = False
End Function

' 检查一个路径是不是目录
' 如果路径不存在，也会返回 `False`
'
' @param path 路径
' @return Boolean True=是目录，False=不是目录(不一定是文件，也许是路径不存在)
Public Function isDir(ByRef path As String) As Boolean
On Error Goto catch:
    isDir = GetAttr(path) = vbDirectory
    Exit Function
catch:
    Err.Clear
    isDir = False
End Function

' 连接路径
' @param pargs()
' @return String
'    result = joinPath("111", "222", "333")
'    Debug.Print result = "111\222\333"
Public Function joinPath(ParamArray pargs()) As String
    joinPath = ""

    Dim argsCount As Integer
    Dim temp As String
    argsCount = UBound(pargs)
    'trimRight
    If argsCount = -1 Then
        '[]
        joinPath = ""
    ElseIf argsCount = 0 Then
        '["aaa"]
        temp = pargs(0)
        temp = UtilsString.trimRight(temp, "\")
        joinPath = temp
    Else
        '["aaa", "bbb"]
        temp = pargs(0)
        temp = UtilsString.trimRight(temp, "\")
        joinPath = temp

        Dim i As Integer
        For i = 1 To argsCount
            temp = pargs(i)

            If UtilsString.endsWith(temp, "\") = True Then
                temp = UtilsString.trimRight(temp, "\")
            End If

            joinPath = joinPath + "\" + temp
        Next
    End If
End Function

' 从路径中获取目录名
' @param path 文件路径
' @return 目录名
Public Function dirname(ByRef path As String) As String
    Dim lastPathSep As Integer
    dirname = UtilsString.trimRight(path, "\")
    lastPathSep = InStrRev(dirname, "\")

    If lastPathSep <> 0 Then
        dirname = Left(dirname, lastPathSep - 1)
    End If
End Function

' 从路径中获取文件名，或者最后一级的文件路径
' @param path 文件路径
' @return 文件名，或最后一级的文件路径
Public Function getBasename(ByRef path As String) As String
    Dim lastPathSep As Integer
    lastPathSep = InStrRev(path, "\")

    If lastPathSep = 0 Then
        getBasename = path
    Else
        getBasename = Right(path, Len(path) - lastPathSep)
    End If
End Function

' 获取文件的短路径（当路径的长度 > 256 时，可以使用该方法转换成段路径）
' @param path 路径字符串
' @return 短路径
Public Function getShortPath(ByRef path As String) As String
    If pathExist(path) Then
        ' 如果路径存在，则直接通过系统内置函数创建
        Dim sShortFile As String
        Dim lResult As Long

        ' 尝试短化，长度为 100 个字符，并获取短化后的路径长度
        sShortFile = Space(100)
        lResult = GetShortPathName(path, sShortFile, 100)

        If lResult > 100 Then
            ' 如果短化后的长度 > 默认值 100，则短化失败（返回值全是空格）
            ' 用真实的短化后路径长度，再次执行短化
            sShortFile = Space(lResult)
            lResult = GetShortPathName(path, sShortFile, lResult)
        End If

        ' 去除多余的空格
        getShortPath = Left$(sShortFile, lResult)
    Else
        ' 如果路径不存在，则返回参数的原始值
        getShortPath = path

        ' 这里不再主动进行路径短化计算，因为文件本身不存在，计算的结果回不准！！！！！
        ' ' 如果路径不存在，则:
        ' ' 1. 最后一级的路径（文件或目录）一定不存在，不对这一级目录做短化
        ' ' 2. 从倒数第二级目录开始迭代
        '
        ' ' 1. 获取最后一级路径，如果存在扩展名，则将扩展名转换成大写字母，并只保留3位
        ' 'Dim endPath as String
        ' endPath = getBasename(path)
        '
        ' ' 2. 从倒数第二级目录开始，从右向左处理
        ' Dim notEatPath As String
        ' notEatPath = UtilsIO.dirname(path)
        ' getShortPath = UtilsIO.getShortPath(notEatPath) & "\" & endPath
    End If
End Function

' 检查指定的路径是不是一个 windows 的绝对路径
' 注意事项
'    只有包含 windows 盘符的路径才能被视为一个绝对路径
'        如 `C:\xxxx`
'
' @param p 需要检查的路径
' @return 指定的路径是不是一个 windows 的绝对路径
Public Function isWinAbsolutePath(ByRef p As String) As Boolean
    isWinAbsolutePath = p Like "[A-Za-z]:\*"
End Function

'===================================================
' 当前 xlsm 工具文件的路径操作
'===================================================
' 获取当前 xlsm 工具文件所在的目录
Public Function getThisWorkbookDir() As String
    getThisWorkbookDir = ThisWorkbook.path
End Function

' 获取当前 xlsm 工具文件所在的目录的短路径
Public Function getThisWorkbookShortDir() As String
    getThisWorkbookShortDir = UtilsIO.getShortPath(UtilsIO.getThisWorkbookDir)
End Function
